feat(alloy-op-evm): post-exec block executor and SDM warming inspector#20213
feat(alloy-op-evm): post-exec block executor and SDM warming inspector#20213
Conversation
d88b6a5 to
4fafc44
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #20213 +/- ##
===========================================
- Coverage 76.1% 0.5% -75.7%
===========================================
Files 184 490 +306
Lines 10551 63483 +52932
===========================================
- Hits 8035 326 -7709
- Misses 2372 63157 +60785
+ Partials 144 0 -144
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
37f0705 to
052c1d6
Compare
| /// Verify canonical gas accounting using an post-exec payload embedded in the block. | ||
| Verify(PostExecPayload), | ||
| /// An post-exec tx was present but invalid, so block execution must fail. | ||
| Invalid, |
There was a problem hiding this comment.
Question to self: Is this enum shared between sequencer and verifiers?
| /// Canonical gas adjustment applied to a transaction when a post-exec refund reduces its gas | ||
| /// cost below the raw EVM result. | ||
| #[derive(Debug, Default, Clone)] | ||
| pub struct PostExecAdjustment { |
There was a problem hiding this comment.
Question to sync review: How are all of these related?
| /// Begin post-exec tracking for the next transaction. | ||
| pub begin_post_exec_tx: fn(&mut Evm, PostExecTxContext), | ||
| /// Extractor for the most recent transaction's exact warming result. | ||
| pub take_last_post_exec_tx_result: fn(&mut Evm) -> PostExecExecutedTx, |
There was a problem hiding this comment.
These functions seem to me be outside of a transaction result. I am also not sure why they are functions instead of just PostExecTxContext and PostExecExecutedTx.
| /// Creates a new [`OpBlockExecutor`]. | ||
| pub fn new(evm: E, ctx: OpBlockExecutionCtx, spec: Spec, receipt_builder: R) -> Self { | ||
| let (post_exec_mode, post_exec_verify_entries, post_exec_invalid_reason) = | ||
| Self::init_post_exec_state(ctx.post_exec_mode.clone()); |
There was a problem hiding this comment.
Could probably take a ref.
| /// Configure how the executor should read the most recent inspector-backed post-exec result. | ||
| pub fn with_post_exec_result( | ||
| mut self, | ||
| take_last_post_exec_tx_result: fn(&mut E) -> PostExecExecutedTx, |
There was a problem hiding this comment.
Why not just define a method directly instead of first assigning it to a field?
pub fn take_last_post_exec_tx_result(&self, &mut E) -> PostExecExecutedTx
| NodeTypes< | ||
| ChainSpec: OpHardforks, | ||
| Primitives: OpPayloadPrimitives, | ||
| Primitives: OpPayloadPrimitives<_TX = OpTransactionSigned>, |
There was a problem hiding this comment.
why does this need to be changed?
| NodeBuilder::new(NodeConfig::new(OP_SEPOLIA.clone())) | ||
| .with_database(create_test_rw_db()) | ||
| .with_types::<OpNode>() | ||
| .with_components( |
There was a problem hiding this comment.
Sure we don't want UniExecutorBuilder here?
| //! still demonstrates the rest of the custom-node surface (custom primitives, executor, engine | ||
| //! API, engine validator, and RPC). | ||
| //! | ||
| //! Downstream forks that previously used `OpPayloadBuilder` with their own `_TX` type will need |
There was a problem hiding this comment.
Could we call the type parameter Transaction or TransactionType and make it default to OpTransactionSigned?
| .executor(CustomExecutorBuilder::default()) | ||
| .payload(BasicPayloadServiceBuilder::new(OpPayloadBuilder::new(false))) | ||
| .network(OpNetworkBuilder::new(false, false)) | ||
| .payload(NoopPayloadServiceBuilder::default()) |
There was a problem hiding this comment.
Not sure I understand why we want to change this example at all. Better make a new distinct example if we want it.
042cd79 to
513dbda
Compare
Add the canonical post-exec block executor, along with the first feature riding on it — SDM (Sequencer-Defined Metering) block-level warming refunds, delivered by the SDMWarmingInspector. - OpBlockExecutor gains three PostExecModes (Disabled / Produce / Verify(payload) / Invalid). Produce accumulates per-tx refund entries for the payload builder to append as a synthetic 0x7D tx; Verify validates an embedded payload against local replay; Disabled is the legacy path (byte-identical to pre-SDM). - SDMWarmingInspector tracks first-warmer provenance for accounts and storage slots, emits exact refund attribution events for every re-touch past the EIP-2929 warm threshold, and suppresses claims from Deposit and synthetic PostExec tx kinds. - Verify-mode validations reject duplicate payload indexes, payload entries targeting deposits or the 0x7D tx itself, and refunds that exceed the tx's raw gas. `apply_pre_execution_changes` debug_asserts the Produce hooks are wired so a downstream fork can't silently drop refunds. - Canonical gas settlement credits the sender, debits the beneficiary and base-fee recipient by the refunded-gas component of their share, and commits the deltas when canonical gas falls below raw gas. - beneficiary_gas_price can legitimately saturate at zero when a legacy tx's gas price equals basefee; inline comment documents the consensus-valid zero case.
Extend op-reth's EvmConfig with the post-exec hooks the new OpBlockExecutor expects. Downstream uses: - The payload builder asks the executor to Produce + drains refund entries via post_exec_executor_for_block + take_post_exec_entries. - The replay RPC asks for the same Produce mode but on a stripped block (0x7D removed) to compare synthesized refunds against the embedded payload. OpEvm auto-wires the SDMWarmingInspector begin/take hooks so callers don't have to plumb them manually; the alloy-op-evm debug_assert guards the failure mode if a downstream fork bypasses OpEvm.
Append a type-0x7D post-exec transaction at the tail of the block when the sequencer builds under --rollup.sdm-enabled. The tx carries the executor's accumulated refund entries as its RLP payload and canonicalizes this node's gas accounting with what a verifier will later independently replay. - OpBuilderConfig/CLI flag (`--rollup.sdm-enabled`) — off by default. When off, the payload path is byte-identical to the pre-feature code. - try_include_post_exec_tx wraps the executor's refund entries in a TxPostExec, executes it, and aborts the payload build with PayloadBuilderError::EvmExecutionError on any synthetic-tx execution failure. Silently dropping it would yield a payload that no honest verifier can reproduce. - Unit tests pin the abort path (should-not-be-Ok-on-failure), the no-entries skip, and the happy-path wrapping of entries. - custom-node example switches to NoopPayloadServiceBuilder; the upstream OpPayloadBuilder is now specialized for OpTransactionSigned to carry the post-exec tx and no longer composes with the example's custom tx type. Doc comment explains what downstream forks need.
…tants - Reject 0x7D txs in Disabled/Invalid modes. Previously `execute_transaction_without_commit` short-circuited any post-exec tx regardless of mode, so a follower with SDM off would silently accept a payload it never validates — state would diverge without a loud failure. - Reject Produce-mode refunds that exceed raw gas used. An inspector over-attribution would otherwise saturate canonical gas to zero and emit an SDMGasEntry that a verifier rejects at pre-execution; failing the payload build here is strictly better than shipping a block the sequencer can't self-verify. - Replace the 2500/2000/2100 magic numbers in SDMWarmingInspector with ACCOUNT_REWARM_REFUND / SLOAD_REWARM_REFUND / SSTORE_REWARM_REFUND and cite the EIP-2929 derivation. - Restore `OpBuilderConfig::new(da_config, gas_limit_config)` to its pre-branch 2-arg signature; add `new_with_sdm` for the SDM-aware constructor so external callers don't break. Tests pin: - Disabled/Invalid modes hard-fail at `execute_transaction` time. - Produce mode rejects a fake over-refund take-hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-struct Collapse the six post-exec fields on OpBlockExecutor (mode, entries, verify_entries, invalid_reason, begin_tx, take_last_tx_result) into a single PostExecState<Evm> field. Public API (with_post_exec_*, set_post_exec_mode, take_post_exec_entries) is preserved — callers in op-reth and the payload builder need no changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ansaction trait Extract the post-exec (type 0x7D) tx construction into a BuildPostExecTransaction trait on reth-optimism-primitives and generalize OpPayloadBuilder, the debug and witness RPCs, OpFullNodeTypes, and try_include_post_exec_tx off the hardcoded `_TX = OpTransactionSigned` bound onto the trait. OpTransactionSigned gets the canonical impl; the custom-node example implements the trait for its own CustomTransaction and drops the payload-builder-breakage note from its docs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ample Implement ConfigurePostExecEvm for CustomEvmConfig and PostExecExecutorExt for CustomBlockExecutor so the example can drive the SDM-enabled OpPayloadBuilder path via BasicPayloadServiceBuilder. Retain RpcAddOns since OpAddOns is hard-bound to OpPrimitives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
45bec29 to
040f652
Compare
Fixes: #20178
Fixes: #20179
This PR add post-execution refund mechanism where the sequencer detects intra-block warming and carries the resulting gas refunds in a 0x7D transaction appended at the end of the block.
Verifiers re-run the block and reject it unless the embedded payload matches what they independently computed - this is probably something we want to remove, but is a good sanity check right now.
SDM is still disabled by default at every layer.
--rollup.sdm-enableddefaults tofalse. The payload builder'swith_sdm_enabled(false)is whatOpNodepasses in. Without this flag, the sequencer's payload path is identical to pre-feature (no 0x7D injection, notry_include_post_exec_txcall).PostExecMode::Disabledis #[derive(Default)]. UnderDisabled,post_exec_refund = 0for every tx, settlement deltas are all zero, and the canonical gas path collapses to the pre-feature behavior.Verifymode. You'd have to opt in via the newcontext_for_block_with_post_exec_modehelper, which isn't wired into any existing pipeline in this branch.End-to-end tests are added in a follow-up PR.
Related: #20216
op-reth/examples/custom-node.UniExecutorBuilderas per comment, and remove#[allow(dead_code)]